package com.peerialism.natcracker.discovery;

import java.io.InputStream;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.List;
import java.util.Properties;
import java.util.StringTokenizer;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.peerialism.natcracker.common.GatewayDevice;
import com.peerialism.natcracker.common.GatewayDevice.GatewayType;

public class ConnectivityInterface {

	private static Logger log = LoggerFactory
			.getLogger(ConnectivityInterface.class);

	private static InetAddress myAddress;

	private static boolean setupConnectionInfo(
			final PeerConnectionInfo connectionInfo, final boolean useSTUN) {

		Properties props = null;
		try {
			/*
			 * Load NAT Discovery property file
			 */
			InputStream resourceAsStream = Thread.currentThread()
					.getContextClassLoader()
					.getResourceAsStream("discovery.props");

			if (resourceAsStream == null) {
				log.error("Could not file property file");
				System.exit(1);
			}

			props = new Properties();
			props.load(resourceAsStream);

		} catch (Exception e) {
			log.error("Could not file property file");
			System.exit(1);
		}
		/*
		 * Test the local IP (if the node is in the open internet, it will have
		 * public IP equal to the local one)
		 */
		myAddress = NetworkUtils.findPrivateAddress();

		if (myAddress == null)
			return false;

		if (myAddress != null && !myAddress.isSiteLocalAddress()) {
			/*
			 * if the address of the node is not local, we report to the
			 * tracker.
			 */
			log.info("Peer has public IP " + myAddress);
			updateConnectionInfo(connectionInfo, myAddress, myAddress,
					GatewayType.OpenInternet, null, true);
		} else {
			log.info("Found Private IP");
		}

		// Check
		if (useSTUN) {
			log.info("Started default FW/NAT behavior discovery process...");

			String stunServersString = props.getProperty("nat.STUN_servers");
			StringTokenizer tokenizer = new StringTokenizer(stunServersString,
					",");
			String[] stunServers = new String[tokenizer.countTokens()];
			for (int x = 0; x < stunServers.length; x++)
				stunServers[x] = tokenizer.nextToken();

			List<DiscoveryInfoBehave> natBehaviorDiscovery = NATBehaviorDiscovery(
					stunServers, myAddress, connectionInfo);
			
			log.debug("Found preferable device: "
					+ connectionInfo.getGatewayDevice().getGatewayType()
					+ ", Behaves: "
					+ connectionInfo.getGatewayDevice().toLongString());

			if (natBehaviorDiscovery.size() > 1) {
				for (DiscoveryInfoBehave discoveryInfoBehave : natBehaviorDiscovery) {
					log.debug("Found also device: "
							+ discoveryInfoBehave.getNatDevice()
									.getGatewayType() + ", Behaves: "
							+ discoveryInfoBehave.getNatDevice().toLongString());
				}
			}
		}

		if (!useSTUN) {
			updateConnectionInfo(connectionInfo, myAddress, myAddress,
					GatewayType.OpenInternet, null, true);
		}
		log.info("Peer has connection info: "
				+ connectionInfo
				+ ", Gateway Device: "
				+ (connectionInfo.getGatewayDevice() == null ? "N/A"
						: connectionInfo.getGatewayDevice().toLongString()));
		return false;
	}

	public static int getAvailablePort(final int min, final int max) {
		for (int port = min; port < max; port++) {
			if (boundable(port)) {
				return port;
			}
		}
		return -1;
	}

	public static boolean boundable(final int port) {
		try {
			final DatagramSocket sock = new DatagramSocket(port);
			sock.close();
			return true;
		} catch (final Exception ex) {
			return false;
		}
	}

	private static List<DiscoveryInfoBehave> NATBehaviorDiscovery(
			String[] stunServers, final InetAddress myAddress,
			final PeerConnectionInfo connectionInfo) {
		/*
		 * Discovery phase
		 */
		log.info("Starting FW/NAT behavior discovery process...");
		/*
		 * Returns an ordered list of accessibility-ordered NAT/firewall devices
		 */
		final List<DiscoveryInfoBehave> discoveryInfos = DiscoveryInterface
				.discover(new InetAddress[] { myAddress }, stunServers);
		log.info("Discovery finished");
		/*
		 * The discovery returns all the STUN information associated with the
		 * interfaces of this machine.
		 */
		/*
		 * The first element in the list is the most accessible one.
		 */
		if (!discoveryInfos.isEmpty()) {
			final DiscoveryInfoBehave discoveryInfo = discoveryInfos.get(0);

			if (discoveryInfo.getNatDevice().getGatewayType() == GatewayType.UdpBlockingFirewall) {
				log.info("The Firewall is blocking all UDP communication, please change the Firewall's configuration.");
				updateConnectionInfo(connectionInfo, myAddress, myAddress,
						GatewayType.UdpBlockingFirewall, null, false);
			} else {
				// log.info("Found " +
				// discoveryInfo.getNatDevice().getGatewayType() + ", Behaves: "
				// + discoveryInfo.getNatDevice().toLongString());
				updateConnectionInfo(connectionInfo,
						discoveryInfo.getLocalIP(),
						discoveryInfo.getPublicIP(), discoveryInfo
								.getNatDevice().getGatewayType(),
						discoveryInfo.getNatDevice(), false);
			}
			return discoveryInfos;
		} else {
			updateConnectionInfo(connectionInfo, myAddress, myAddress,
					GatewayType.UdpBlockingFirewall, null, false);
			log.warn("NAT Behavior discovery failed");
			return null;
		}
	}

	private static void updateConnectionInfo(
			final PeerConnectionInfo connectionInfo,
			final InetAddress localAddress, final InetAddress publicAddress,
			final GatewayType gatewayType, final GatewayDevice device,
			final boolean findBoundablePort) {
		connectionInfo.setCftpPublicIp(publicAddress);
		if (device == null) {
			connectionInfo.setNATDevice(new GatewayDevice());
		}
		connectionInfo.setNATDevice(device);
		connectionInfo.setGatewayType(gatewayType);
		connectionInfo.getGatewayDevice().setGatewayIp(publicAddress);
		connectionInfo.setCftpPublicIp(localAddress);

	}

	/**
	 * Discovers the public IP of the client, using UPnP or STUN, and updates
	 * the specified {@link PeerConnectionInfo}
	 * 
	 * @return true if a public interface was found, false otherwise.
	 */
	public static boolean discover(final PeerConnectionInfo connectionInfo) {
		final boolean useSTUN = true;
		return setupConnectionInfo(connectionInfo, useSTUN);
	}

}
